/**
 * 
 */
package image.header;

import image.Image;

import java.nio.ByteBuffer;
import java.util.ArrayList;

import codec.JPEGQuantizationTable;
import codec.huffman.JPEGCustomizedHuffmanTable;
import codec.huffman.JPEGHuffmanTable;
import codec.huffman.JPEGStandardHuffmanTable;

import util.Log;

/**
 * @author nquangtrung
 *
 */
public class JPEGHeader extends Header {
	public static final String TAG = "JPEGHeader";
	
	public static final byte SEG_START_OF_IMAGE 				= (byte) 0xD8;
	public static final byte SEG_START_OF_FRAME_BASELINE 		= (byte) 0xC0;
	public static final byte SEG_START_OF_FRAME_PROGRESSIVE 	= (byte) 0xC2;
	public static final byte SEG_HUFFMAN_TABLE					= (byte) 0xC4;
	public static final byte SEG_QUANTIZATION_TABLE				= (byte) 0xDB;
	public static final byte SEG_RESTART_INTERVAL				= (byte) 0xDD;
	public static final byte SEG_START_OF_SCAN					= (byte) 0xDA;
	public static final byte SEG_COMMENT						= (byte) 0xFE;
	public static final byte SEG_END_OF_IMAGE					= (byte) 0xD9;
	public static final byte SEG_JFIF							= (byte) 0xE0;
	
	public static final int OPTIMIZED_HUFFMAN_TABLE = 1;
	public static final int STANDARD_HUFFMAN_TABLE 	= 0;
	public static final int DEFAULT_HUFFMAN_TABLE 	= STANDARD_HUFFMAN_TABLE;
	
	private int width;
	private int height;
	
	private boolean downsample;
	private boolean cache;
	private double quality;
	
	private ArrayList<Integer> cacheData;
	
	private JPEGHuffmanTable huffmanTable;
	private int[][]	quantizationTable;
	/**
	 * 
	 */
	public JPEGHeader() {
		this.setDownsample(true);
	}
	
	public boolean hasCache() {
		return !this.getCacheData().isEmpty();
	}
	public void emptyCache() {
		this.getCacheData().clear();
	}
	public void cache(int word) {
		if (isCache())
			this.getCacheData().add(word);
	}
	
	public void setHuffmanTableType(int type, Image image) {
		switch(type) {
		case STANDARD_HUFFMAN_TABLE:
			this.setHuffmanTable(new JPEGStandardHuffmanTable());
			break;
		case OPTIMIZED_HUFFMAN_TABLE:
			this.setHuffmanTable(new JPEGCustomizedHuffmanTable(image, this));
			break;
		}
	}

	@Override
	public byte[] getByte() {		
		// SOI
		byte[] SOI 
			= {(byte) 0xFF, SEG_START_OF_IMAGE};
		// APP0
		byte[] APP0
			= {(byte) 0xFF, SEG_JFIF,
				0x00, 0x10,									// Length
				0x4A, 0x46, 0x49, 0x46, 0x00,				// "JFIF"
				0x01, 0x02,									// version 1.2
				0x00,										// aspect ratio
				0x00, 0x64, 0x00, 100,						// density 100x100
				0x00, 0x00									// thumbnail 0x0
				};
		// SOF0
		byte[] SOF0 
			= {(byte) 0xFF, SEG_START_OF_FRAME_BASELINE,  	// Marker
				0x00, 0x11,									// Length = 8 + components * 3
				0x08,										// Bit depth
				(byte) (this.getHeight() >> 8), 
				(byte) (this.getHeight()),					// Height
				(byte) (this.getWidth() >> 8), 
				(byte) (this.getWidth()),					// Width
				0x03,										// Number of component
				// Y component
				0x01, // Y
				0x11, // Sampling factor
				0x00, // Quantization table
				// Cb component
				0x02, 0x11, 0x01, 
				// Cr component
				0x03, 0x11,	0x01
			};
		
		// Huffman table
		ByteBuffer dhtBuffer = ByteBuffer.allocate(2 + 17 * 4 + 12 * 2 + 162 * 2 + 1000);
		dhtBuffer.position(0);
		dhtBuffer.put((byte) 0xFF); dhtBuffer.put(SEG_HUFFMAN_TABLE);
		dhtBuffer.put((byte) 0x01);		// length 
		dhtBuffer.put((byte) 0xA2);
		
		JPEGHuffmanTable huftable = this.getHuffmanTable();
		int size = 0;
		for (int component = 0; component < 4; component++) {			
			int[] bits = huftable.getBITS(component);
			Log.debugArray(TAG, "BITS", bits, 11);
			int[] hufval = huftable.getHUFVAL(component);
			byte table_type = 0x00;
			switch(component) {
			case JPEGHuffmanTable.LUMINANCE_DC: table_type = 0x00; break;
			case JPEGHuffmanTable.LUMINANCE_AC: table_type = 0x10; break;
			case JPEGHuffmanTable.CHROMINANCE_DC: table_type = 0x01; break;
			case JPEGHuffmanTable.CHROMINANCE_AC: table_type = 0x11; break;
			}
			dhtBuffer.put(table_type);
			size += 1;
			for (int i = 0; i < bits.length; i++) {
				dhtBuffer.put((byte) bits[i]);
				size += 1;
			}
			for (int i = 0; i < hufval.length; i++) {
				dhtBuffer.put((byte) hufval[i]);
				size += 1;
			}
		}
		byte[] DHT = new byte[size + 4];
		dhtBuffer.position(0);
		dhtBuffer.get(DHT, 0, DHT.length);
		DHT[2] = (byte) ((size + 2) >> 8);
		DHT[3] = (byte) (size + 2);	
		byte[] DQT
			= {(byte) 0xFF, SEG_QUANTIZATION_TABLE,			// Marker
				0x00, (byte) 0x84,							// Length = 2 + 65 * 2 = 132
				0x00,										// Byte, 1st quantization table
				// Reserved
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x01,										// Byte, 2nd quantization table
				// Reserved
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
				0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
			};
		JPEGQuantizationTable.fitByte(DQT,  5, JPEGQuantizationTable.LUMINANCE, 8, 8, this.getQuality());
		JPEGQuantizationTable.fitByte(DQT, 70, JPEGQuantizationTable.CHROMINANCE, 8, 8, this.getQuality());
		
		// Last, put SOS marker
		byte[] SOS 
			= {(byte) 0xFF, SEG_START_OF_SCAN,					// SOS marker
				0x00, 0x0C,										// scan header length = 6 + 2 * 3 = 0x0B
				0x03,											// Number of components
				// Component Y
				0x01, 
				0x00, // Huffman table for Luminance
				// 0x00, (byte) 0xFF, 0x00,
				// Component Cb
				0x02, 
				0x11, // Huffman table for Chrominance
				// 0x00, (byte) 0xFF, 0x00,
				// Component Cb
				0x03, 
				0x11, // Huffman table for Chrominance
				0x00, (byte) 0x3F, 0x00
				};
		Log.debugArray(TAG, "SOI: ", SOI, 9);
		Log.debugArray(TAG, "APP0: ", APP0, 9);
		Log.debugArray(TAG, "SOF0: ", SOF0, 9);
		Log.debugArray(TAG, "DHT: ", DHT, 9);
		Log.debugArray(TAG, "DQT: ", DQT, 9);
		Log.debugArray(TAG, "SOS: ", SOS, 9);
		size = SOI.length + SOS.length + SOF0.length + DHT.length + DQT.length + APP0.length;
		ByteBuffer bb = ByteBuffer.allocate(size);
		bb.put(SOI);
		bb.put(APP0);
		bb.put(DQT);
		bb.put(SOF0);
		bb.put(DHT);
		bb.put(SOS);
		byte[] b = new byte[size];
		bb.position(0);
		bb.get(b, 0, size);
		
		return b;
	}

	/**
	 * @return the width
	 */
	public int getWidth() {
		return width;
	}

	/**
	 * @param width the width to set
	 */
	public void setWidth(int width) {
		this.width = width;
	}

	/**
	 * @return the height
	 */
	public int getHeight() {
		return height;
	}

	/**
	 * @param height the height to set
	 */
	public void setHeight(int height) {
		this.height = height;
	}

	/**
	 * @return the huffmanTable
	 */
	public JPEGHuffmanTable getHuffmanTable() {
		return huffmanTable;
	}

	/**
	 * @param huffmanTable the huffmanTable to set
	 */
	public void setHuffmanTable(JPEGHuffmanTable huffmanTable) {
		this.huffmanTable = huffmanTable;
	}

	/**
	 * @return the quantizationTable
	 */
	public int[][] getQuantizationTable() {
		return quantizationTable;
	}

	/**
	 * @param quantizationTable the quantizationTable to set
	 */
	public void setQuantizationTable(int[][] quantizationTable) {
		this.quantizationTable = quantizationTable;
	}

	/**
	 * @return the quality
	 */
	public double getQuality() {
		return quality;
	}

	/**
	 * @param quality the quality to set
	 */
	public void setQuality(double quality) {
		this.quality = quality;
	}

	/**
	 * @return the downsample
	 */
	public boolean isDownsample() {
		return downsample;
	}

	/**
	 * @param downsample the downsample to set
	 */
	public void setDownsample(boolean downsample) {
		this.downsample = downsample;
	}

	/**
	 * @return the cache
	 */
	public boolean isCache() {
		return cache;
	}

	/**
	 * @param cache the cache to set
	 */
	public void setCache(boolean cache) {
		this.cache = cache;
	}

	/**
	 * @return the cacheData
	 */
	public ArrayList<Integer> getCacheData() {
		return cacheData;
	}

	/**
	 * @param cacheData the cacheData to set
	 */
	public void setCacheData(ArrayList<Integer> cacheData) {
		this.cacheData = cacheData;
	}
}
